package main

import (
	"encoding/json"
	"fmt"
	"reflect"

	"github.com/fatih/structs"
)

type UserInfo struct {
	Name string `json:"name"`
	Age  int    `json:"age"`
}

func NewUserInfo() {
	u1 := UserInfo{
		Name: "小猪",
		Age:  20,
	}
	fmt.Println(u1)
}

// 假设现在要将上面的u1转换成map[string]interface{}，该如何操作呢？
func changeStructToMap() {
	u1 := UserInfo{
		Name: "小猪",
		Age:  21,
	}
	b, _ := json.Marshal(&u1)
	var m map[string]interface{}
	json.Unmarshal(b, &m)
	for k, v := range m {
		fmt.Printf("key:%v value:%v\n", k, v)
	}
}

// 看起来没什么问题，但其实这里是有一个“坑”的。
// 那就是Go语言中的json包在序列化空接口存放的数字类型（
// 整型、浮点型等）都会序列化成float64类型。
// 也就是上面例子中m["age"]现在底层是一个float64了，不是个int了。我们来验证下：

func changeStructToMap2() {
	u1 := UserInfo{
		Name: "小猪",
		Age:  21,
	}
	b, _ := json.Marshal(&u1)
	var m map[string]interface{}
	json.Unmarshal(b, &m)
	for k, v := range m {
		fmt.Printf("key:%v value:%v type:%T\n", k, v, v)
	}
}

// 很显然，出现了一个意料之外的行为，我们需要想办法规避掉。
// 反射
func toMap(in interface{}, tagName string) (map[string]interface{}, error) {
	out := make(map[string]interface{})

	v := reflect.ValueOf(in)
	fmt.Println("v:", v)
	// 判断是否是结构体指针
	if v.Kind() == reflect.Ptr {
		v = v.Elem()
		fmt.Println("v.Elem():", v)
	}

	if v.Kind() != reflect.Struct { // 非结构体返回错误提示
		return nil, fmt.Errorf("ToMap only accepts struct or struct pointer; got %T", v)
	}

	t := v.Type()
	// 遍历结构体字段
	// 指定tagName值为map中key;字段值为map中value
	for i := 0; i < v.NumField(); i++ {
		fi := t.Field(i)
		if tagValue := fi.Tag.Get(tagName); tagValue != "" {
			out[tagValue] = v.Field(i).Interface()
		}
	}
	return out, nil
}

// 没办法就需要自己动手去实现了。这里使用反射遍历结构体字段的方式生成map，具体代码如下：
func changeStructToMap3() {
	u1 := UserInfo{
		Name: "小猪",
		Age:  21,
	}
	var m map[string]interface{}
	m, _ = toMap(&u1, "json")
	for k, v := range m {
		fmt.Printf("key:%v value:%v type:%T\n", k, v, v)
	}
}

// 第三方库 structs
type UserInfo2 struct {
	Name string `json:"name" structs:"name"`
	Age  int    `json:"age" structs:"age"`
}

func changeStructToMap4() {
	u1 := UserInfo2{
		Name: "小猪",
		Age:  21,
	}
	m3 := structs.Map(&u1)
	for k, v := range m3 {
		fmt.Printf("key:%v value:%v type:%T\n", k, v, v)
	}
}

// structs本身是支持嵌套结构体转map[string]interface{}的，
// 遇到结构体嵌套它会转换为map[string]interface{}嵌套map[string]interface{}的模式。
type UserInfo3 struct {
	Name    string `json:"name" structs:"name"`
	Age     int    `json:"age" structs:"age"`
	Profile `json:"profile" structs:"profile"`
}

type Profile struct {
	Hobby string `json:"hobby" structs:"hobby"`
}

func changeStructToMap5() {
	u1 := UserInfo3{
		Name:    "小猪",
		Age:     34,
		Profile: Profile{"双色球"},
	}
	m3 := structs.Map(&u1)
	for k, v := range m3 {
		fmt.Printf("key:%v value:%v type:%T\n", k, v, v)
	}
}

// 从结果来看最后嵌套字段profile是map[string]interface {}，属于map嵌套map。
// 使用反射转成单层map
// 如果我们想把嵌套的结构体转换成一个单层map该怎么做呢？
// 我们把上面反射的代码稍微修改一下就可以了：
// ToMap2 将结构体转为单层map
func ToMap2(in interface{}, tag string) (map[string]interface{}, error) {

	// 当前函数只接收struct类型
	v := reflect.ValueOf(in)
	if v.Kind() == reflect.Ptr { // 结构体指针
		v = v.Elem()
	}
	if v.Kind() != reflect.Struct {
		return nil, fmt.Errorf("ToMap only accepts struct or struct pointer; got %T", v)
	}

	out := make(map[string]interface{})
	queue := make([]interface{}, 0, 1)
	queue = append(queue, in)

	for len(queue) > 0 {
		v := reflect.ValueOf(queue[0])
		if v.Kind() == reflect.Ptr { // 结构体指针
			v = v.Elem()
		}
		queue = queue[1:]
		t := v.Type()
		for i := 0; i < v.NumField(); i++ {
			vi := v.Field(i)
			if vi.Kind() == reflect.Ptr { // 内嵌指针
				vi = vi.Elem()
				if vi.Kind() == reflect.Struct { // 结构体
					queue = append(queue, vi.Interface())
				} else {
					ti := t.Field(i)
					if tagValue := ti.Tag.Get(tag); tagValue != "" {
						// 存入map
						out[tagValue] = vi.Interface()
					}
				}
				break
			}
			if vi.Kind() == reflect.Struct { // 内嵌结构体
				queue = append(queue, vi.Interface())
				break
			}
			// 一般字段
			ti := t.Field(i)
			if tagValue := ti.Tag.Get(tag); tagValue != "" {
				// 存入map
				out[tagValue] = vi.Interface()
			}
		}
	}
	return out, nil
}

func main() {
	// NewUserInfo()
	// changeStructToMap()
	// changeStructToMap2()
	// changeStructToMap3()
	// changeStructToMap4()
	changeStructToMap5()

}
